Skip to content

gh-154115: Fix typing._tp_cache calling the wrapped function twice on TypeError#154119

Open
fedonman wants to merge 3 commits into
python:mainfrom
fedonman:fix-gh-154115-tp-cache-typeerror
Open

gh-154115: Fix typing._tp_cache calling the wrapped function twice on TypeError#154119
fedonman wants to merge 3 commits into
python:mainfrom
fedonman:fix-gh-154115-tp-cache-typeerror

Conversation

@fedonman

@fedonman fedonman commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

_tp_cache wraps typing's __getitem__ methods with an lru_cache and falls back to calling the original function when it sees a TypeError, on the assumption that the TypeError came from hashing an unhashable argument. A TypeError raised by the wrapped function itself was caught the same way, so with hashable arguments the function ran once inside the cache and then a second time through the fallback before the error propagated.

You can see it directly:

import typing

calls = []

@typing._tp_cache
def f(x):
    calls.append(x)
    raise TypeError("unrelated to hashability")

try:
    f("value")
except TypeError:
    pass

print(calls)  # ['value', 'value']

and through the public API, for example typing.List[int, str] runs its whole __getitem__ twice before raising.

The fix only takes the fallback when the arguments really are unhashable. It rebuilds the same cache key that lru_cache builds (functools._make_key, which also folds in the argument types when typed=True) and re-raises when that key is hashable, so a TypeError coming from the wrapped function is no longer turned into a second call. The successful path is unchanged.

…ice on TypeError

_tp_cache falls back to calling the wrapped function directly when the
arguments are unhashable, which it detected by catching TypeError from the
lru_cache lookup.  A TypeError raised by the wrapped function itself was
caught the same way, so with hashable arguments the function ran once inside
the cache, was caught, and then ran a second time through the fallback before
the error propagated.  This was reachable through the public API, for example
typing.List[int, str] evaluated its whole __getitem__ twice.

Only fall back when the arguments really are unhashable, by rebuilding the
same cache key lru_cache uses, and re-raise otherwise so the function is not
run a second time.
@fedonman

Copy link
Copy Markdown
Contributor Author

@zware part of EuroPython sprint.

Comment thread Lib/test/test_typing.py Outdated
…riations

Both tests exercised the same positional and keyword call variations but
handled them inconsistently: one cleared the calls list between them, the
other asserted the combined result.  Loop over the variations with subTest
and clear the list each iteration so each variation is asserted in isolation.

@JelleZijlstra JelleZijlstra left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not convinced we should change anything here. The issue description doesn't show any evidence that this causes practical problems, and the proposed code change makes things more complicated.

@fedonman

Copy link
Copy Markdown
Contributor Author

@JelleZijlstra Should I close the PR then?

@JelleZijlstra

Copy link
Copy Markdown
Member

We can wait for some other opinions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: Todo

Development

Successfully merging this pull request may close these issues.

4 participants